BearZPY Blog

Hi, nice to meet you

BearZPY's avatar BearZPY

Java 判断字节流是否是 UTF8 编码

Java 判断字节流是否是 UTF8 编码

遇到本来设计时使用 GBK 编码处理的地方,在实际使用过程导入了 UTF8 编码,造成了显示文本为乱码的现象,在了解 UTF8,GBK 编码和 Unicode 标准之后,编写了 Java 判断字节流是否是 UTF8 编码的程序,如果是 UTF8 编码,则转换成 GBK 编码。

编码的基础知识

Unicode 是一种标准,GBK 和 UTF8 是具体是编码格式。Java 的字符都是以 Unicode 进行存储的,占两或四个字节(看版本,且 Unicode 编码中对应关系是存在 0x00 的编码的)。Java 中的 getBytes() 方法是和平台(编码)相关的,在中文系统中返回的可能是 GBK 或 GBK2312,在英文系统中返回的可能是 ISO-8859-1。

  • Unicode 标准:是计算机科学领域里的一项业界标准,包括字符集、编码方案等,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
  • GBK 编码:汉字内码扩展规范,国标,汉字占两个字节。
  • UTF8 编码:针对 Unicode 的可变长度字符编码,用 1 到 6 个字节编码 Unicode 字符,汉字一般占 3 个字节。

UTF8 编码格式

如果 Unicode 字符由 2 个字节表示,则编码成 UTF8 很可能需要 3 个字节。而如果 Unicode 字符由 4 个字节表示,则编码成 UTF8 可能需要 6个字节。用 4 个或 6 个字节去编码一个 Unicode 字符可能太多了,但很少会遇到那样的 Unicode 字符。

UTF8 编码规则:如果只有一个字节则其最高二进制位为 0,如果是多字节,其第一个字节从最高位开始,连续的二进制位值为 1,1 的个数决定了其编码的字节数,其余各字节均以 10 开头。

// Unicode6.1定义范围:0~10 FFFF
// 20 0000 ~ 3FF FFFF 和 400 0000 ~ 7FFF FFFF 属于 UCS-4,UTF8 现在已经弃用了这部分内容

---------------------------------------------------------------------------------
n | Unicode (十六进制)    | UTF - 8 (二进制)
--+-----------------------+------------------------------------------------------
1 | 0000 0000 - 0000 007F | 0xxxxxxx
2 | 0000 0080 - 0000 07FF | 110xxxxx 10xxxxxx
3 | 0000 0800 - 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
4 | 0001 0000 - 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
---------------------------------------------------------------------------------
// 以下部分弃用
5 | 0020 0000 - 03FF FFFF | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6 | 0400 0000 - 7FFF FFFF | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
---------------------------------------------------------------------------------

Java 如何判断单个字符编码是否是 UTF8

假设当前需要判定一个 byte[] 数组内的编码是否是 UTF8 编码,这个 byte[] 是 String 通过 getBytes() 方法获取的,判断单个字符的编码步骤如下:

  • 从 byte[] 数组中获取一个 byte 并将它转换成无符号类型的 int 变量 value
  • 判断 value 是否是 ASCII 字符(小于 0x80)
  • 判断 value 是否是无效字符(大于 0x80,小于 0xC0,参照 UTF8 编码规则)
  • 确认该字符编码的是几字节 UTF8
  • 确认该字符编码的除第一个字节外的字节是否满足 10xxxxxx 格式

PS:

Java getBytes() 获取的是带符号的十六进制,实际处理时需要使用无符号十六进制。

GBK 和 UTF8 中 ASCII 字符的值是一样的。

具体程序

将十六进制流中的所有编码按照单个判定的方式便利一遍,如果有不符合 UTF8 编码规则的字符出现,则该十六进制流就不是 UTF8 编码格式的字串。

public static int byteToUnsignedInt(byte data) {
    return data & 0xff;
}

public boolean isUTF8(byte[] pBuffer) {
    boolean IsUTF8 = true;
    boolean IsASCII = true;
    int size = pBuffer.length;
    int i = 0;
    while (i < size) {
        int value = byteToUnsignedInt(pBuffer[i]);
        if (value < 0x80) {
            // (10000000): 值小于 0x80 的为 ASCII 字符
            if (i >= size - 1) {
                if (IsASCII) {
                    // 假设纯 ASCII 字符不是 UTF 格式
                    IsUTF8 = false;
                }
                break;
            }
            i++;
        } else if (value < 0xC0) {
            // (11000000): 值介于 0x80 与 0xC0 之间的为无效 UTF-8 字符
            IsASCII = false;
            IsUTF8 = false;
            break;
        } else if (value < 0xE0) {
            // (11100000): 此范围内为 2 字节 UTF-8 字符
            IsASCII = false;
            if (i >= size - 1) {
                break;
            }

            int value1 = byteToUnsignedInt(pBuffer[i + 1]);
            if ((value1 & (0xC0)) != 0x80) {
                IsUTF8 = false;
                break;
            }

            i += 2;
        } else if (value < 0xF0) {
            IsASCII = false;
            // (11110000): 此范围内为 3 字节 UTF-8 字符
            if (i >= size - 2) {
                break;
            }

            int value1 = byteToUnsignedInt(pBuffer[i + 1]);
            int value2 = byteToUnsignedInt(pBuffer[i + 2]);
            if ((value1 & (0xC0)) != 0x80 || (value2 & (0xC0)) != 0x80) {
                IsUTF8 = false;
                break;
            }

            i += 3;
        }  else if (value < 0xF8) {
            IsASCII = false;
            // (11111000): 此范围内为 4 字节 UTF-8 字符
            if (i >= size - 3) {
                break;
            }

            int value1 = byteToUnsignedInt(pBuffer[i + 1]);
            int value2 = byteToUnsignedInt(pBuffer[i + 2]);
            int value3 = byteToUnsignedInt(pBuffer[i + 3]);
            if ((value1 & (0xC0)) != 0x80
                || (value2 & (0xC0)) != 0x80
                || (value3 & (0xC0)) != 0x80) {
                IsUTF8 = false;
                break;
            }

            i += 3;
        } else {
            IsUTF8 = false;
            IsASCII = false;
            break;
        }
    }

    return IsUTF8;
}

UTF8 编码转 GBK 编码

// Unicode
String unicodeString = "张三";
// 获取 UTF8 编码
byte[] nameUTF8 = unicodeString.getBytes("utf-8");
// UTF8 编码转 str
String str = new String(name, "utf-8");
// 获取 GBK 编码
byte[] nameGBK = str.getBytes("gbk");